package com.optimatch.model.solver;

import java.util.ArrayList;
import java.util.List;

import com.optimatch.ui.controler.observers.SolverObserver;

import choco.Choco;
import choco.Options;
import choco.cp.model.CPModel;
import choco.cp.solver.CPSolver;
import choco.cp.solver.search.BranchingFactory;
import choco.cp.solver.search.integer.branching.AssignVar;
import choco.cp.solver.search.integer.valiterator.IncreasingDomain;
import choco.cp.solver.search.integer.valselector.MaxVal;
import choco.cp.solver.search.integer.valselector.MinVal;
import choco.cp.solver.search.integer.varselector.MinValueDomain;
import choco.cp.solver.search.integer.varselector.ratioselector.DomOverWDegSelector;
import choco.kernel.common.logging.ChocoLogging;
import choco.kernel.common.logging.Verbosity;
import choco.kernel.common.util.tools.ArrayUtils;
import choco.kernel.model.variables.integer.IntegerVariable;


public class SolverProbleme extends CPModel {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	
	private static final int N_EQUIPES = 12;
	
	//Le nombre d'equipes participant dans le tournoi. Toujours PAIR !
	private int nombreEquipes;
	//Le nombre total de matchs, egal au nombre d'equipes -1
	private int nombreMatchs;
	
	private List<Triplet> matchsDefinisUtilisateurs;
	private List<ContrainteHome> lieuDesMatchs;
	
	private IntegerVariable[][] matchs;	//Les matchs ayant lieu entre les équipes, de dimension nombreEquipes*nombreMatchs
	private IntegerVariable[][] home;	//Variable permettant de savoir chez qui le match a lieu
	private IntegerVariable[][] breaks_vp;//Chaque break pour chaque equipe
	private IntegerVariable[] breaksParEquipe; //Le nombre de breaks par equipe
	private IntegerVariable nombreTotalbreaks;
	
	private SolverObservable sObservable;
	private CPSolver solver;
	/**
	 * Constructeur par défaut avec un nombre fixe N_EQUIPES d'équipes
	 */
	public SolverProbleme() {
		super();
		this.nombreEquipes = N_EQUIPES;
		this.nombreMatchs = nombreEquipes - 1;
		this.matchsDefinisUtilisateurs = new ArrayList<Triplet>();
		this.lieuDesMatchs = new ArrayList<ContrainteHome>();
		matchs = Choco.makeIntVarArray("matchs", nombreEquipes, nombreMatchs, 0, nombreEquipes-1);
		home = Choco.makeIntVarArray("home", nombreEquipes, nombreMatchs, 0, 1);
		breaks_vp = Choco.makeIntVarArray("breaks", nombreEquipes, nombreMatchs-1, 0, 1);
		breaksParEquipe = Choco.makeIntVarArray("breaksEquipe", nombreEquipes, 0, nombreMatchs-1);
		nombreTotalbreaks = Choco.makeIntVar("breaksEquipes", nombreEquipes-2,nombreEquipes*(nombreMatchs-1));
		sObservable = new SolverObservable();
		this.postConstraints();
	}
	
	/**
	 * 
	 * @param nEquipes le nombre d'équipes du tournoi (pair)
	 * @param contraintesMatchs les matchs fixés entre les équipes (-1 si pas de match fixé)
	 * @param contraintesHome les lieux des matchs fixés
	 */
	public SolverProbleme(int nEquipes, 
			List<Triplet> contraintesMatchs,
			List<ContrainteHome> contraintesHome) {
		super();
		if(nEquipes%2!=0) {
			System.out.println("Le nombre d'équipes doit être pair ! Le programme se termine...");
			System.exit(0);
		}
		this.nombreEquipes = nEquipes;
		this.nombreMatchs = nombreEquipes - 1;
		this.matchsDefinisUtilisateurs = contraintesMatchs;
		this.lieuDesMatchs = contraintesHome;
		matchs = Choco.makeIntVarArray("matchs", nombreEquipes, nombreMatchs, 0, nombreEquipes-1);
		home = Choco.makeIntVarArray("home", nombreEquipes, nombreMatchs, 0, 1);
		breaks_vp = Choco.makeIntVarArray("breaks", nombreEquipes, nombreMatchs-1, 0, 1);
		breaksParEquipe = Choco.makeIntVarArray("breaksEquipe", nombreEquipes, 0, nombreMatchs-1);
		nombreTotalbreaks = Choco.makeIntVar("breaksEquipes", nombreEquipes-2,nombreEquipes*(nombreMatchs-1));
		sObservable = new SolverObservable();
		this.postContraintesMatchsDefinis(contraintesMatchs);
		this.postContraintesLieuDefini(contraintesHome);
		this.postConstraints();
	}

	/**
	 * Ajoute toutes les contraintes
	 */
	public void postConstraints() {
		
		for(int i = 0 ; i < nombreEquipes; i++) {
			for(int j = 0; j < nombreMatchs;j++){
				//Une equipe ne peut jouer contre elle-meme
				matchs[i][j].removeVal(i);
			}
		}
		postMatchConstraints();
		postHomeConstraints();
		postBreakConstraints();
		postRedundantConstraints();
		postSymmetries();
	}

	/**
	 * Une équipe doit rencontrer exactement une fois toutes les autres équipes
	 */
	private void postMatchConstraints() {
		
		/*
		 * Propage les contraintes sur les rencontres et les variables home lors d'une affectation
		 */
		/*Integer[] parameters = {nombreEquipes};
		IntegerVariable[] home_flatten = new IntegerVariable[nombreEquipes*nombreMatchs];
		IntegerVariable[] matchs_flatten = new IntegerVariable[nombreEquipes*nombreMatchs];
		IntegerVariable[] variables = new IntegerVariable[2*(nombreEquipes*nombreMatchs)];
		for (int i = 0; i < nombreEquipes; i++) {
			for (int j = 0; j < nombreMatchs; j++) {
				home_flatten[i*nombreMatchs+j] = home[i][j];
				matchs_flatten[i*nombreMatchs+j] = matchs[i][j];
				variables[i*nombreMatchs+j] = matchs_flatten[i*nombreMatchs+j];
				variables[nombreEquipes*nombreMatchs + (i*nombreMatchs+j)] = home_flatten[i*nombreMatchs+j];
			}
		}	
		addConstraint(new ComponentConstraint(PropagatorManager.class, parameters, variables));*/
		
		// L'equipe i doit jouer une fois contre tous les autres equipes
		for(int i = 0; i < nombreEquipes; i++){
			addConstraint(Options.C_ALLDIFFERENT_AC, Choco.allDifferent(matchs[i]));
		}
		
		//Equipe i joue contre equipe j au match k <=> equipe j joue contre equipe i au match k
		for (int k = 0; k < nombreMatchs; k++) {
			addConstraint(Choco.inverseChanneling(
					ArrayUtils.getColumn(matchs, k), 
					ArrayUtils.getColumn(matchs, k)));	
		}
		
	}
	
	/**
	 * Si une équipe A se déplace chez une autre équipe B, 
	 * cela veut dire que l'équipe B reste chez elle et vice-versa
	 */
	private void postHomeConstraints() {
		
		// Equipe i est chez elle au match k <=> equipe jouant contre i est dehors au match k 
        for(int k = 0; k < nombreMatchs; k++) {
            for(int i = 0; i < nombreEquipes; i++) {
                IntegerVariable rivalAtHome = new IntegerVariable("",0,1);
                addConstraint(Choco.nth(matchs[i][k], ArrayUtils.getColumn(home, k), rivalAtHome));
               //post rivalAtHome = home[matchs[i][k]][k] puisque rival de i au jour k = matchs[i][k]
                
                //Contraintes equivalentes, mais en propagation?
                //addConstraint(Choco.eq(Choco.plus(home[i][k], rivalAtHome), 1));
				addConstraint(
						Choco.ifOnlyIf(
							Choco.eq(home[i][k], 1), 
							Choco.eq(rivalAtHome, 0))
						);
            }
        }
	}
	
	/**
	 *Comptabilise le nombre de breaks par équipe 
	 *Il est possible d'avoir au plus nombreMatchs-1 breaks par equipe sur le total des matchs joués par cette dernière
	 */
	private void postBreakConstraints() {

		for (int i = 0; i < nombreEquipes; i++) {
			for (int j = 0; j < nombreMatchs-1; j++) {
				addConstraint(
						Choco.ifOnlyIf(
								Choco.neq(Choco.plus(home[i][j], home[i][j+1]), 1),
								//Choco.eq(home[i][j], home[i][j+1]), //Moins bon
								Choco.eq(breaks_vp[i][j], 1)
								)
						);
			}
			addConstraint(Choco.eq(Choco.sum(breaks_vp[i]), breaksParEquipe[i]));
		}
		addConstraint(Choco.eq(Choco.sum(breaksParEquipe), nombreTotalbreaks));
		
		//Le nombre de breaks est toujours pair pour une paire de jours consécutifs
		//(du aux nEquipes/2 matchs à domicile et nEquipes/2 à l'extérieur)...
		for (int i = 0; i < nombreMatchs-1; i++) {
			addConstraint(Choco.eq(Choco.mod(Choco.sum(ArrayUtils.getColumn(breaks_vp, i)), 2), 0));
		}
		
		//...et par conséquence le nombre de breaks total est pair
		for (int i = nombreEquipes-1; i <= nombreEquipes*(nombreMatchs-1); i+=2) {
			nombreTotalbreaks.removeVal(i);
		}
	}	
	
	/**
	 * Contraintes redondantes pouvant améliorer la propagation
	 */
	private void postRedundantConstraints() {
		
		for(int i = 0; i < nombreMatchs ; i++){
			//Une equipe joue contre une seule equipe par jour
			addConstraint(Options.C_ALLDIFFERENT_AC, Choco.allDifferent(ArrayUtils.getColumn(matchs, i)));
		
			//Chaque jour la moitié des équipes se déplacent tandis que l'autre moitié est à domicile
			addConstraint(Choco.eq(Choco.sum(ArrayUtils.getColumn(home,i)), (int) (nombreEquipes/2d)));
			
		}		
		
	}
	
	private void postSymmetries() {
		
		/*
		 * Ideas : 
		 * 1) Pour la 1ere equipe, ordonner les matchs 
		 * 2) Contraindre le nombre de breaks de la 1ere equipe à être inférieur ou égal au nombre de breaks de la seconde
		 * 3) Le 1er match de la 1ere equipe est "at home"
		 */
		if(matchsDefinisUtilisateurs.isEmpty()) {
			for (int i = 0; i < nombreMatchs-1; i++) {
				addConstraint(Choco.lt(matchs[0][i], matchs[0][i+1])); 
			}
		}
		
		if(lieuDesMatchs.isEmpty()) {
			for (int i = 0; i < nombreEquipes-1; i++) {
				for (int j = i+1; j < nombreEquipes; j++) {
					addConstraint(Choco.leq(breaksParEquipe[i], breaksParEquipe[j]));
				}
			}
			addConstraint(Choco.eq(home[0][0], 1)); 
		}
	}
	
	/**
	 * Stratégie de branchement
	 * 1) Le nombre de breaks total : valeur min
	 * 2) Le nombre de breaks par équipe : variable de plus petit domaine, valeur min
	 * 3) Les breaks, considérés un par un :  domOverWDeg :
	 * To each variable are associated, at any time, three values: 
	 *  - dom the current domain size, 
	 *  - deg the current number of uninstantiated constraints involving the variable, 
	 *  - w the sum of the counters associated with these deg constraints. 
	 *  The strategy selects the variable with the smallest ratio ri = dom/w ∗ deg.
	 *  4) home, domWDeg
	 *  5) matchs, domWDeg
	 * @param s le solver du problème
	 */
	public void heuristic(CPSolver s) {

		IntegerVariable[] home_flatten = new IntegerVariable[nombreEquipes*nombreMatchs];
		IntegerVariable[] matchs_flatten = new IntegerVariable[nombreEquipes*nombreMatchs];
		IntegerVariable[] breaks_vp_flatten = new IntegerVariable[nombreEquipes*(nombreMatchs-1)];
		for (int i = 0; i < nombreEquipes; i++) {
			for (int j = 0; j < nombreMatchs; j++) {
				home_flatten[i*nombreMatchs+j] = home[i][j];
				matchs_flatten[i*nombreMatchs+j] = matchs[i][j];
			}
			for (int j = 0; j < nombreMatchs-1; j++) {
				breaks_vp_flatten[i*(nombreMatchs-1)+j] = breaks_vp[i][j];
			}
		}
		
		s.addGoal(BranchingFactory.minDomMinVal(s, s.getVar(new IntegerVariable[]{nombreTotalbreaks})));
		
		s.addGoal(new AssignVar(
				new MinValueDomain(s, s.getVar(breaksParEquipe)), 
				new MinVal() )
		);

		s.addGoal(new AssignVar(
				new DomOverWDegSelector(s, s.getVar(breaks_vp_flatten)),
				new MaxVal()
				));
		s.addGoal( BranchingFactory.domWDeg(s, s.getVar(home_flatten), new IncreasingDomain()));
		s.addGoal( BranchingFactory.domWDeg(s, s.getVar(matchs_flatten), new IncreasingDomain()));		

	}	

	/**
	 * Créé un solver, lit le modèle et applique l'heuristique de branchement. 
	 * Résoud (pas de minimisation puisque le branching commence par le nombre
	 * de breaks minimal)
	 * @return le solver mentionné ci-dessus
	 */
	public CPSolver solve() {
		solver = new CPSolver();
		solver.read(this);
		this.heuristic(solver); 
		ChocoLogging.setVerbosity(Verbosity.SOLUTION);
		solver.solve(); 
		sObservable.update( this);
		return solver;
	}

	/**
	 *
	 * @param solver
	 * @return une String représentant le nombre de breaks total et par équipe, 
	 * les matchs et les variables home/extérieur
	 */
	public String toString(CPSolver solver) {

		String str = "\n\nNombre total de breaks : "+solver.getVar(nombreTotalbreaks).getVal()+"\n\n";
		for (int i = 0; i < nombreEquipes; i++) {
			str += "Nombre de breaks Equipe "+i+" : "+solver.getVar(breaksParEquipe[i]).getVal()+"\n";
		}
		str += "\nMatchs : \n\n";
		for (int i = 0; i < nombreEquipes; i++) {
			str += "Equipe "+(i)+" : ";
			for (int j = 0; j < nombreMatchs; j++) {
				str += solver.getVar(matchs[i][j]).getVal()+" ";
			}
			str += "\n";
			
		}
		str += "\nMatchs home ?\n\n";
		for (int i = 0; i < nombreEquipes; i++) {
			str += "Equipe "+(i)+" : ";
			for (int j = 0; j < nombreMatchs; j++) {
				str += solver.getVar(home[i][j]).getVal()+" ";
			}
			str += "\n";
		}
		return str;
	}
	/**
	 * 
	 * @param contraintesUtilisateurs les jours possibles pour un match entre deux équipes
	 */
	private void postContraintesMatchsDefinis(List<Triplet> contraintesUtilisateurs){
		if(!contraintesUtilisateurs.isEmpty()) {
			for (Triplet t : contraintesUtilisateurs) {
				for (int jour = 0; jour<nombreMatchs; jour++) {
					if(!t.getJoursDeRencontrePossible().contains(jour)) {
						addConstraint(Choco.neq(matchs[t.getEquipe1()][jour], t.getEquipe2()));
						addConstraint(Choco.neq(matchs[t.getEquipe2()][jour], t.getEquipe1()));
					}
				}
			}
		}
	}
	
	/**
	 * 
	 * @param contraintesHome les contraintes de lieu des matchs
	 */
	private void postContraintesLieuDefini(List<ContrainteHome> contraintesHome) {

		for(ContrainteHome c : contraintesHome) {
	        for(int k = 0; k < nombreMatchs; k++) {
				addConstraint(
						Choco.implies(
								Choco.eq(matchs[c.getEquipe1()][k], c.getEquipe2()),
								Choco.eq(home[c.getEquipe1()][k], c.getHome()))
						);
				addConstraint(
						Choco.implies(
							Choco.neq(home[c.getEquipe1()][k], c.getHome()), 
							Choco.neq(matchs[c.getEquipe1()][k], c.getEquipe2()))
						);
	        }
		}
		
	}
	
	/**
	 * Observer et creation de la classe Solution pour sauvegarder les donnes
	 * @param solverObserver
	 */

	public int[] getAllMatchsParJournee(int journee){
		int[] matchesJournee = new int[nombreEquipes];
		for(int i = 0; i < nombreEquipes ; i++)
			matchesJournee[i] = solver.getVar(matchs[i][journee]).getVal();
		return matchesJournee;
	}
	public int[] getAllMatchsParEquipe(int equipe){
		int[] matchsEquipe = new int[nombreMatchs];
		for(int i = 0; i < nombreMatchs ; i++)
			matchsEquipe[i] = solver.getVar(matchs[equipe][i]).getVal();
		return matchsEquipe;
	}
	public int[] getDomicileParJournee(int journee){
		int[] domicileJournee = new int[nombreEquipes];
		for(int i = 0; i < nombreEquipes ; i++)
			domicileJournee[i] = solver.getVar(home[i][journee]).getVal();
		return domicileJournee;
	}
	public int[] getDomicileParEquipe(int equipe){
		int[] domicileEquipe = new int[nombreMatchs];
		for(int i = 0; i < nombreMatchs ; i++)
			domicileEquipe[i] = solver.getVar(home[equipe][i]).getVal();
		return domicileEquipe;
	}
	public int[][] getAllMatchs(){
		int[][] allMatchs = new int[nombreEquipes][nombreMatchs];
		for(int i = 0; i < nombreEquipes; i++)
			allMatchs[i] = getAllMatchsParEquipe(i);
		return allMatchs;
	}
	public int[][] getAllDomicile(){
		int[][] allDomicile = new int[nombreEquipes][nombreMatchs];
		for(int i = 0; i < nombreEquipes; i++)
			allDomicile[i] = getDomicileParEquipe(i);
		return allDomicile;
	}
	public double getTempsResolution(){
		return solver.getTimeCount();
	}
	public int getNombreBackTracks(){
		return solver.getBackTrackCount();
	}
	public int getBreaks(){
		return solver.getVar(nombreTotalbreaks).getVal();
	}
	

	public int getNombreEquipes() {
		return nombreEquipes;
	}

	public int getNombreMatchs() {
		return nombreMatchs;
	}

	public void addObserver(SolverObserver solverObserver) {
		// TODO Auto-generated method stub
		this.sObservable.addObserver(solverObserver);
	}

}